Item 2: 尽量使用 const、enum、inline替换 #define

这个 Item 改名为 用 编译器 取代 预处理器 也许更好一些,因为 #define 或许不被视为语言的一部分。这正是问题所在。

const 常量代替 #define

#define ASPECT_RATIO 1.653

记号名称ASPECT_RATIO在编译器开始处理代码之前就被预处理器替换掉了,导致编译器没看见ASPECT_RATIO这个记号名称,从而ASPECT_RATIO没进入记号表(symbol table)。

当运用此变量出现编译错误时,错误信息只会提到1.653而不是ASPECT_RATIO,如果ASPECT_RATIO定义在一个非你所写的头文件内,则很难追踪。

该问题也可能出现在记号式调试(symbolic debugger)中,原因相同:名称可能并未进入到记号表。

解决办法是使用一个常量替换宏:

const double AspectRatio = 1.653;

使用常量替换 #define 的好处:

  • 语言常量一定会被编译器看到,因此能够进入记号表内
  • 使用常量可能会比用 #define 使用更少的空间,原因是预处理器执行的是宏替换,可能导致目标码(object code)出现多份1.653

使用常量替换 #define 的两种特殊情况:

  • 定义常量指针,若要指针所指内容不变则需要两次使用 const
  • class专属常量:
    • 为了将常量的作用域限制于class内,必须让它成为class的一个成员
    • 为了确保此常量只有一份实体,必须让它成为一个static成员
//常量指针
const char* const authorName = "Scott Meyers";

//类专属常量
class GamePlayer {
private:
  static const int NumTurns = 5;      //常量声明
  int scores[NumTurns];               //常量使用
  ...
};

NumTurns 是 declaration(声明),而不是 definition(定义)

通常,C++ 要求你为你使用的任何东西都提供一个定义,但是一个 static 整型(例如:int,char,bool)的类专属常量是一个例外。只要你不去取得它们的地址,你可以只声明并使用它,而不提供它的定义。如果你要取得一个类专属常量的地址,或者你使用的编译器在你没有取得地址时也要求定义的话(该行为不是正确的),就必须单独提供定义:

const int GamePlayer::NumTurns;
  • 应该把它放在一个实现文件而非头文件中
  • 因为类专属常量的初始值在声明时已经提供初值,定义时不可以再设初值
  • 定义时不需要 static 关键字

没有办法使用 #define 来创建一个类专属常量,因为 #defines 不考虑作用域。一旦一个宏被定义,它就在其后的编译过程有效(除非在某处被 #undef)。除此之外,#define 不能够提供任何封装性,没有 private #define。

the enum hack

旧式编译器也许不支持staitc成员在声明时获得初值,就算支持,也只有整数常量支持在类内获取初值。

#include <iostream>
using namespace std;

class test {
	static const int cnt = 5;
	static const double;
};

const int test::cnt;
const double test::hh = 1.1;

int main() {
	return 0;
}

如果类里有数组,编译器必须在编译期间知道数组大小,如果编译器不支持static整数常量在类内获取初值,则可以使用enum。

一个属于枚举类型的数值可充当 int 被使用:

class GamePlayer {
private:
  enum { NumTurns = 5 };//"the enmu hack" —— 令NumTurns成为5的一个记号名称
  int scores[NumTurns];
  ...
};

enum hack 的行为某方面说比较像 #define 而不像 const ,事实上 enum hack 是模板元编程的基础技术。

可以合法地取得一个 const 的地址,但不能合法地取得一个 enum 的地址,这正像同样不能合法地取得一个 #define 的地址。

如果你不希望人们得到你的整数常量的指针或引用,枚举就是强制约束这一点的好方法。

inline 函数代替 #define

//以 a 和 b 的较大值调用函数 f
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

当写出这种宏时,必须为每一个实参加上小括号,否则可能在表达式调用这个宏时可能遇到麻烦。

即使加上了小括号,仍有不可思议的事情发生:

int a = 5, b = 0;
CALL_WITH_MAX(++a, b); 		//a 被累加了2次
CALL_WITH_MAX(++a, b+10); 	//a 被累加了1次

可以通过模板内联函数获得宏的效率以及一般函数的所有可预料行为和类型安全性:

template<typename T>    
inline void callWithMax(const T& a, const T& b) 
{                                       
	f(a > b ? a : b);                        
}

因为 callWithMax 是一个真正的函数,它遵循函数的作用域和访问规则。例如绝对可以写出一个类内的私有内联函数,而宏一般无法做到。

总结

  • 有了const、enum 和 inline ,对预处理器的需求降低了,但并非完全消除,#include、#ifdef/#ifndef 仍然很重要

  • 对于简单常量,使用 const 代替宏定义,其优点:

    • const 常量能够出现在符号表中,方便调试
    • 宏定义因为进行的宏替换,有时候会造成代码冗余,const 常量能够很好的避免这个问题
    • const 常量可以作为类专属成员,#define 则毫无封装性
    • 整型的类专属常量可以在类中声明时直接初始化
  • enum 也可以作为整型常量使用,并且无法取得其地址

  • 使用内联函数代替宏定义的函数将会在不损失效率的情况下降低发生错误的可能性